<?php

require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Exception.php';

class ClassStructure_Mixin extends ClassStructure_Abstract {
		
    const MIXIN_DELIMITER = '__';
    
    /**
	 * An array of mixins stored for dynamic inheritance and method invocation
	 * 
	 * Each mixin is indexed using a unique key - which may be the class name
     * of the mixin, or the registered mixin key.
	 * 
	 * @access private
	 * @var array
	 */
	private $arr_mixins = array();
    
    /**
     * An array of methods available for this object via the mixins
     *
     * @access private
     * @var array
     */
    private $arr_methods = array();
	
    /**
     * A registry of initialised mixins, indexed by an mixin key.
     *
     * When adding an mixin, the programmer can use the registered mixin
     * key for easy mixin management.
     *
     * @static static
     * @access private
     * @var array
     */
    static private $arr_registered_mixins = array();
    
    /***************************************************************************
    * Object Construction
    ***************************************************************************/
    
	/**
	 * Constructor:  default (blank) constructor.
	 * 
	 * @access public
	 * @param mixed $mix_mixins An array of valid mixins.
	 * @return object
	 */
    public function __construct($mix_mixins = array()) {
        parent::__construct();
        
        $this->__addMixins($mix_mixins);
    }
    
    /**
     * Determines whether the supplied parameter is an instance of the target.
     *
     * This method uses the PHP operator instanceof.
     * 
     * @access public
     * @param string|object $mix_class The class name or object to compare.
     * @return boolean Whether the supplied parameter is an instance of the base
     *      layer object.
     */
    public function __isInstanceOf($mix_class) {
        
        $bln_instance_of = parent::__isInstanceOf($mix_class);
        
        if(!$bln_instance_of) {
            
            // standardise parameter into a class name
            $str_class = is_object($mix_class)
                ? $this->__getClassName($mix_class)
                : (string) $mix_class;
            
            $arr_mixins = $this->__getMixins();
            foreach($arr_mixins as $str_mixin_key => $obj_mixin) {
                
                $obj_mixin = ($obj_mixin instanceof self)
                    ? $obj_mixin->__getObject()
                    : $obj_mixin;
                
                if($obj_mixin instanceof $str_class) {
                    return true;
                }
                
            }
            
        }
        
        return $bln_instance_of;
        
    }
    
    /**
     * Determines whether the supplied parameter is a subclass of the target.
     *
     * This method uses the PHP function is_subclass_of.
     * 
     * @access public
     * @param string|object $mix_class The class name or object to compare.
     * @return boolean Whether the supplied parameter is a subclass of the target.
     */
    public function __isSubclassOf($mix_class) {
        
        $bln_subclass_of = parent::__isSubclassOf($mix_class);
        
        if(!$bln_subclass_of) {
            
            // standardise parameter into a class name
            $str_class = is_object($mix_class)
                ? $this->__getClassName($mix_class)
                : (string) $mix_class;
            
            $arr_mixins = $this->__getMixins();
            foreach($arr_mixins as $str_mixin_key => $obj_mixin) {
                
                $obj_mixin = ($obj_mixin instanceof self)
                    ? $obj_mixin->__getObject()
                    : $obj_mixin;
                
                if(
                    is_subclass_of($obj_mixin, $str_class) ||
                    (get_class($obj_mixin) == $str_class)
                ) {
                    return true;
                }
                
            }
            
        }
        
        return $bln_subclass_of;
        
    }
    
    public function __hasMethod($str_method) {
        
        // if no mixin delimiter is found, the client script must have been
        // using a transparent method invocation technique; thus, try to find
        // the corresponding mixin method name
        if(strpos($str_method, self::MIXIN_DELIMITER) === false) {
            $str_method = $this->__findMixinMethodByName($str_method);
        }
        
        return $this->hasMixinMethod($str_method);
        
    }
    
    /**
     * Gets the class methods of the object and its mixins.
     *
     * This method uses the PHP function get_class_methods().
     * 
     * @access public 
     * @param string $str_virtual_prefix The prefix to give methods that are not
     *      actually defined at compile time.
     * @return string The class name of the base layer object.
     */
    public function __getClassMethods($str_virtual_prefix = '') {
        
        $arr_class_methods = array();
        
        $arr_methods = get_class_methods($this);
        foreach($arr_methods as $str_method) {
            $arr_class_methods[$str_method] = $str_method;
        }
        
        
        $arr_mixins = $this->__getMixins();
        foreach($arr_mixins as $str_mixin_key => $obj_mixin) {
            
            $arr_methods = ($obj_mixin instanceof self)
                ? $obj_mixin->__getClassMethods()
                : get_class_methods($obj_mixin);
                
            foreach($arr_methods as $str_method) {
                if(strpos($str_method, '__') !== 0) {
                    $str_method = $str_virtual_prefix . $str_method;
                    $arr_class_methods[$str_method] = $str_method;
                }
            }
            
        }
        
        return array_values($arr_class_methods);
        
    }
    
    /**
     * Gets the class method signatures of the object and its mixins.
     *
     * This method uses the PHP function get_class_methods().
     * 
     * @access public 
     * @param string $str_virtual_prefix The prefix to give methods that are not
     *      actually defined at compile time.
     * @return string The class name of the base layer object.
     */
    public function __getMethodSignatures($str_virtual_prefix = '@') {
        
        $arr_signatures = array();
        
        $arr_mixins = $this->__getMixins();
        foreach($arr_mixins as $str_mixin_key => $obj_mixin) {
                
            $arr_methods = self::getMethodSignatures($obj_mixin);
            foreach($arr_methods as $str_method => $arr_method) {
                $arr_method['visibility'] = 'virtual';
                $arr_signatures[$str_method] = self::formatMethodSignature($str_method, $arr_method);
            }
            
        }
        
        $arr_methods = self::getMethodSignatures($this);
        foreach($arr_methods as $str_method => $arr_method) {
            $arr_signatures[$str_method] = self::formatMethodSignature($str_method, $arr_method);
        }
        
        return $arr_signatures;
        
    }
    
    /***************************************************************************
    * Management of Mixins
    ***************************************************************************/
    
    /**
	 * Adds a new mixin to the current mixin collection
	 * 
	 * @access protected
	 * @param mixed $mix_mixin The mixin key or object to use as an mixin.
     * @param string $str_mixin_key The specific mixin key to use to name
     *      this particular mixin.
	 * @return void
	 */
	protected function __addMixin($mix_mixin, $str_mixin_key = null) {
        $this->attachMixin($mix_mixin, $str_mixin_key);
	}
    
    /**
     * Adds multiple mixins to the current mixin collection.
     *
     * If the array index is of a string datatype, then this will be used as the
     * specific mixin key.
     *
     * @access protected
     * @param mixed $mix_mixins An array of valid mixins.
     * @return void
     */
    protected function __addMixins($mix_mixins) {

        // ensure this is an array
        $arr_mixins = !is_array($mix_mixins) ? (array) $mix_mixins : $mix_mixins;
        
        // iterate through mixin array and add each individually
        foreach($arr_mixins as $i => $mix_mixin) {
            
            // if the array index is a string, use it as the mixin's key
            $str_mixin_key = is_string($i) ? $i : null;
            $this->__addMixin($mix_mixin, $str_mixin_key);
            
        }

    }
    
    /**
     * This is the core method to attach an mixin.  
     *
     * Attaching an mixin will assign a reference to the internal mixin
     * array list, as well as parse the mixin object for callable methods
     * to expose at this object's callable interface.
     *
     * @access private
     * @param mixed $mix_mixin The mixin key or object to use as an mixin.
     * @param string $str_mixin_key The specific mixin key to use to name
     *      this particular mixin.
	 * @return void
     */
    private function attachMixin($mix_mixin, $str_mixin_key = null) {
        
        $obj_mixin = null;
        
        // case: an mixin key is supplied, so lookup the registered mixins
        if(is_string($mix_mixin)) {
            
            $obj_mixin = self::constructMixin($mix_mixin);
            
            // if the specific mixin key is null, then default to the 
            // registered mixin type
            if(empty($str_mixin_key)) {
                $str_mixin_key = $mix_mixin;
            }
            
        } 
        
        // case: an object is supplied, so use this directly
        else if(is_object($mix_mixin)) {
            
            $obj_mixin = $mix_mixin;
            
            // if the specific mixin key is null, then default to the
            // object's class name
            if(empty($str_mixin_key)) {
                $str_mixin_key = get_class($mix_mixin);
            }
            
        }
        
        // ensure there is a valid mixin defined
        if(!is_object($obj_mixin)) {
            throw new ClassStructure_Exception_InvalidMixinDefinition('The method [' . __METHOD__ . '] expects a valid mixin by means of a string-based mixin key or an object.');
        }
        
        // store the mixin
		$this->arr_mixins[$str_mixin_key] = $obj_mixin;
        
        // retrieve the available methods and their corresponding signatures
        $arr_methods_available = $this->parseAvailableMethods($obj_mixin);
        
        // iterate through each method and prepend the mixin key (to make it
        // unique and easy for lookup
        $arr_methods = array();
        foreach($arr_methods_available as $str_method => $str_signature) {
            $arr_methods[$str_mixin_key . self::MIXIN_DELIMITER . $str_method] = $str_signature;
        }
        
        // merge the available methods with the existing mixin methods
        $this->arr_methods = array_merge($this->arr_methods, $arr_methods);
        
    }
    
    /**
     * Gets a particular mixin, keyed by the mixin key
     *
     * @access public
     * @param string $str_mixin_key The mixin's key.
     * @return object
     */
    public function __getMixin($str_mixin_key) {
        $obj_mixin = null;
        if($this->__hasMixin($str_mixin_key)) {
            $obj_mixin = $this->arr_mixins[$str_mixin_key];
        }
        return $obj_mixin;
    }
    
    /**
     * Gets all mixins, keyed by the mixin key
     *
     * @access public
     * @return array
     */
    public function __getMixins() {
        return $this->arr_mixins;
    }
    
    /**
     * Determines whether a particular mixin exists within this object
     *
     * @access public
     * @param string $str_mixin_key The mixin's key.
     * @return boolean
     */
    public function __hasMixin($str_mixin_key) {
        return array_key_exists($str_mixin_key, $this->arr_mixins);
    }
    
    /**
     * Finds a matching mixin method for the supplied method.  
     *
     * This method can be overwritten to encapsulate better or specific logic.
     * All this method does it convert a method into a possible mixin method
     * definition (ie. prepending the mixin key).
     *
     * This particular implementation will find the first instance of a match
     * based solely on the method name.
     *
     * @access protected
     * @param string $str_method The method name to match.
     * @param array $arr_parameters The method's parameters, gathered in array format.
     * @return string
     */
    protected function __findMixinMethodByName($str_method) {
        
        // prepend the mixin delimiter to help find the mixin method
        // using only strpos.
        $str_target = self::MIXIN_DELIMITER . $str_method;
        
        // iterate through all the mixin methods and return the first matching
        // entry based on strpos.
        foreach($this->arr_methods as $str_mixin_method => $str_signature) {
            if(strpos($str_mixin_method, $str_target) !== false) {
                return $str_mixin_method;
            }
        }
        
        return null;
    }
    
    /**
     * Finds a matching mixin method for the supplied method.
     *
     * This particular implementation will find the first instance of a match
     * based solely on the method name.
     *
     * @access protected
     * @param string $str_method The method name to match.
     * @param array $arr_parameters The method's parameters, gathered in array format.
     * @return string
     */
    protected function __findMixinMethod($str_method, $arr_parameters = array()) {
        return $this->__findMixinMethodByName($str_method);
    }
    
    /***************************************************************************
    * Management of Method Invocations
    ***************************************************************************/
    
    /**
     * Delegates the invoked method to the defined mixins.
     *
     * The return value of the mixin will be returned.
     *
     * @final final
     * @access protected
     * @param string $str_method The method name to invoke, intended for the internal object.
     * @param array $arr_parameters The method's parameters, gathered in array format.
     * @return mixed
     */
    final protected function delegateMethod($str_method, $arr_parameters = array()) {
        
        $mix = null;
		
        $str_method_original = $str_method;
        
        $obj_mixin = null;
        $str_mixin_method = null;
        
        // if no mixin delimiter is found, the client script must have been
        // using a transparent method invocation technique; thus, try to find
        // the corresponding mixin method name
        if(strpos($str_method, self::MIXIN_DELIMITER) === false) {
            $str_method = $this->__findMixinMethod($str_method, $arr_parameters);
            if(empty($str_method)) {
                throw new ClassStructure_Exception_MethodDoesNotExist($str_method_original, $this);
            }
        }
        
        // case: method name has the format 'mixinName__methodName'
        if(preg_match('/.*' . self::MIXIN_DELIMITER . '.*/i', $str_method) !== false) {
          
            $arr_method = explode(self::MIXIN_DELIMITER, $str_method);
            $str_mixin_key = isset($arr_method[0]) ? $arr_method[0] : null;
            $str_mixin_method = isset($arr_method[1]) ? $arr_method[1] : null;
            
            if(empty($str_mixin_key) || empty($str_mixin_method)) {
                throw new ClassStructure_Exception_MethodDoesNotExist($str_method_original, $this);
            }
            
            // ensure the mixin method is defined - if not, try to 
            // automatically add the mixin
            if(!$this->hasMixinMethod($str_method)) {
                $this->__addMixin($str_mixin_key);
            }
            
            // retrieve the mixin
            $obj_mixin = $this->__getMixin($str_mixin_key);
            
        }
        
        // if there exists an mixin and the mixin's specific method
        if(!is_null($obj_mixin) && !is_null($str_mixin_method)) {
            
            // ensure the method is callable
            if(!method_exists($obj_mixin, $str_mixin_method) && !method_exists($obj_mixin, '__call')) {
                throw new ClassStructure_Exception_MethodDoesNotExist($str_mixin_method, $obj_mixin);
            }
            
            // invoke the method on the mixin object			
            $mix = call_user_func_array(array($obj_mixin, $str_mixin_method), $arr_parameters);
        
        }
        
		return $mix;
        
    }
    
    /***************************************************************************
    * Collection of Utility Methods
    ***************************************************************************/
    
    /**
     * Determines whether an mixin method definition exists
     *
     * @access private
     * @param string $str_method The mixin method definition
     * @return boolean
     */
    private function hasMixinMethod($str_method) {
        return array_key_exists($str_method, $this->arr_methods);
    }
    
    /***************************************************************************
    * Management of Registered Mixins
    ***************************************************************************/
    
    /**
     * Constructs an mixin object as defined from a registered key.
     *
     * This method will try to use the Zend_Loader_Autoloader class to ensure
     * the class is defined before initialising an object.  Otherwise, programmers
     * must ensure that the class is already declared.
     *
     * @static static
     * @access protected
     * @param string $str_mixin_key The registered mixin key.
     * @return object
     */
    static protected function constructMixin($str_mixin_key) {
        
        $obj = null;
        
        // ensure this is a registered mixin
        if(!self::isRegisteredMixin($str_mixin_key)) {
            throw new ClassStructure_Exception_RegisteredMixinDoesNotExist('The supplied mixin key [' . $str_mixin_key . '] does not exists in the mixin registry - use [' . __CLASS__ . '::registerMixin] to registry the key first.');
        }
        
        // get the associated class for the mixin key
        $str_class = self::$arr_registered_mixins[$str_mixin_key];

        // try to use Zend to autoload the class
        if(in_array('Zend_Loader_Autoloader', get_declared_classes())) {
            Zend_Loader_Autoloader::autoload($str_class);
        }
        
        
        if(!class_exists($str_class, true)) {
            throw new ClassStructure_Exception_ClassDoesNotExist('The class [' . $str_class . '] has not yet been declared, and failed to autoload, which has caused an exception while trying to construct the registered mixin [' . $str_mixin_key . '].');
        }
        
        // create the mixin object
        $obj = new $str_class();
        
        return $obj;
    }
    
    /**
     * Determines whether the provided key is a registered key.
     *
     * @static static
     * @access public
     * @param string $str_mixin_key The registered mixin key.
     * @return boolean
     */
    static public function isRegisteredMixin($str_mixin_key) {
        return isset(self::$arr_registered_mixins[$str_mixin_key]);
    }

    /**
     * Register an mixin key for future mixin construction.
     *
     * @static static
     * @access public
     * @param string $str_mixin_key The mixin key.
     * @param string $str_class The class name associated with the key.
     * @return void
     */
    static public function registerMixin($str_mixin_key, $str_class) {
        self::$arr_registered_mixins[$str_mixin_key] = $str_class;
    }
    
    /**
     * Remove an mixin key from the registry.
     *
     * @static static
     * @access public
     * @param string $str_mixin_key The mixin key to remove.
     * @return void
     */
    static public function unregisterMixin($str_mixin_key) {
        unset(self::$arr_registered_mixins[$str_mixin_key]);
    }

    static public function getRegisteredMixins() {
        return self::$arr_registered_mixins;
    }
}

ClassStructure_Mixin::registerMixin('property', 'ClassStructure_Mixin_Property');

?>
